package scene.shape;

import java.util.Arrays;
import java.util.List;

import raytracing.HitRecord;
import raytracing.Interval;
import raytracing.Ray;
import scene.shape.geometry.Plane;
import scene.shape.geometry.Primitive;
import scene.shape.geometry.Triangle;
import util.Vector3f;

public class BoundingBox {
	
	public Vector3f lower, upper; //front-left-bottom, back-right-up
	
	public BoundingBox(Vector3f lower, Vector3f upper){
		this.lower = new Vector3f(lower.x, lower.y, lower.z);
		this.upper = new Vector3f(upper.x, upper.y, upper.z);
	}
	
	public BoundingBox(BoundingBox bbox){
		this(bbox.lower, bbox.upper);
	}
	
	/**
	 * Construct axis-aligned boundingbox of a list of shapes
	 */
	public static BoundingBox computeBoundingBox(List<Primitive> shapes){
		float minx = Float.POSITIVE_INFINITY, miny = Float.POSITIVE_INFINITY, minz = Float.POSITIVE_INFINITY,
				maxx = Float.NEGATIVE_INFINITY,maxy = Float.NEGATIVE_INFINITY,maxz = Float.NEGATIVE_INFINITY;
		for(Primitive p : shapes){
				if(p.getBoundingBox() == null)
					continue;
				Vector3f lower = p.getBoundingBox().lower,
						 upper = p.getBoundingBox().upper;
				minx = Math.min(minx, lower.x);
				miny = Math.min(miny, lower.y);
				minz = Math.min(minz, lower.z);
				maxx = Math.max(maxx, upper.x);
				maxy = Math.max(maxy, upper.y);
				maxz = Math.max(maxz, upper.z);
			}
		return new BoundingBox(new Vector3f(minx,miny,minz), new Vector3f(maxx,maxy,maxz));
	}
	
	public static BoundingBox computeBoundingBoxFromTriangleList(List<Triangle> triangles){
		float minx = Float.POSITIVE_INFINITY, miny = Float.POSITIVE_INFINITY, minz = Float.POSITIVE_INFINITY,
				maxx = Float.NEGATIVE_INFINITY,maxy = Float.NEGATIVE_INFINITY,maxz = Float.NEGATIVE_INFINITY;
		for(Triangle t : triangles){
			minx = Math.min(minx,Math.min(t.v1.x, Math.min(t.v2.x, t.v3.x)));
			maxx = Math.max(maxx,Math.max(t.v1.x, Math.max(t.v2.x, t.v3.x)));
			miny = Math.min(miny,Math.min(t.v1.y, Math.min(t.v2.y, t.v3.y)));
			maxy = Math.max(maxy,Math.max(t.v1.y, Math.max(t.v2.y, t.v3.y)));
			minz = Math.min(minz,Math.min(t.v1.z, Math.min(t.v2.z, t.v3.z)));
			maxz = Math.max(maxz,Math.max(t.v1.z, Math.max(t.v2.z, t.v3.z)));
		}
		return new BoundingBox(new Vector3f(minx,miny,minz), new Vector3f(maxx,maxy,maxz));
	}
	
	/**
	 * CODE TESTEN
	 * @param other
	 * @return
	 */
	public boolean overlaps(BoundingBox other){
		//TODO CODE TESTEN
		Vector3f m = this.lower, n = this.upper, o = other.lower, p = other.upper;
		return 	 !(m.x > p.x
				|| o.x > n.x
				|| m.y > p.y
				|| o.y > n.y
				|| m.z > p.z
				|| o.z > n.z);
	}
	
	public boolean contains(Vector3f p){
		return  lower.x <= p.x && p.x <= upper.x
				&& lower.y <= p.y && p.y <= upper.y
				&& lower.z <= p.z && p.z <= upper.z;
	}
	
	public float getVolume(){
		return (upper.x - lower.x) * (upper.y - lower.y) * (upper.z - lower.z);
	}
	
	public float getSizeX(){
		return upper.x - lower.x;
	}
	
	public float getSizeY(){
		return upper.y - lower.y;
	}
	
	public float getSizeZ(){
		return upper.z - lower.z;
	}
	
	public Interval getRangeX(){
		return new Interval(lower.x,upper.x);
	}
	
	public Interval getRangeY(){
		return new Interval(lower.y,upper.y);
	}
	
	public Interval getRangeZ(){
		return new Interval(lower.z,upper.z);
	}
	
	public boolean intersects(Ray r){
		float txmin, txmax, tymin, tymax, tzmin, tzmax;
		float ax = 1 / r.getDirection().x,
			  ay = 1 / r.getDirection().y,
			  az = 1 / r.getDirection().z;
		if(ax >= 0){
			txmin = ax * (lower.x - r.getE().x);
			txmax = ax * (upper.x - r.getE().x);
		}else{
			txmin = ax * (upper.x - r.getE().x);
			txmax = ax * (lower.x - r.getE().x);
		}
		if(ay >= 0){
			tymin = ay * (lower.y - r.getE().y);
			tymax = ay * (upper.y - r.getE().y);
		}else{
			tymin = ay * (upper.y - r.getE().y);
			tymax = ay * (lower.y - r.getE().y);
		}
		if(az >= 0){
			tzmin = az * (lower.z - r.getE().z);
			tzmax = az * (upper.z - r.getE().z);
		}else{
			tzmin = az * (upper.z - r.getE().z);
			tzmax = az * (lower.z - r.getE().z);
		}
		if(txmin > tymax || tymin > txmax || txmin > tzmax || tymin > tzmax || tzmin > txmax || tzmin > tymax)
			return false;
		return true;
	}
	
	public HitRecord getIntersectionPoint(Ray r, float mint, float maxt){
		//r = new Ray(invTransform.multiply(new Vector4f(r.getE(),1f)).get3DVector(), invTransform.multiply(new Vector4f(r.getDirection(),0)).get3DVector());
		if(!intersects(r))
			return HitRecord.NOHIT;
		Plane upPlane = new Plane(upper, new Vector3f(0,1,0)),
			  bottomPlane = new Plane(lower, new Vector3f(0,1,0)),
			  backPlane = new Plane(lower, new Vector3f(0,0,1)),
			  frontPlane = new Plane(upper, new Vector3f(0,0,1)),
			  leftPlane = new Plane(lower, new Vector3f(1,0,0)),
			  rightPlane = new Plane(upper, new Vector3f(1,0,0));
		List<Plane> planes = Arrays.asList(upPlane,bottomPlane, backPlane, frontPlane, leftPlane, rightPlane);
		HitRecord closestHit = HitRecord.NOHIT;;
		for(Plane p : planes){
			HitRecord hr = p.intersects(r, mint, maxt);
			float t = hr.getT();
			if(hr.hasHit() && t > mint && t < maxt){
				maxt = hr.getT();
				closestHit = hr;
			}
		}
		return closestHit;
	}

	public BoundingBox scale(float f) {
		return new BoundingBox(lower.scale(f), upper.scale(f));
	}

	public void checkMinMax(Vector3f v) {
		lower.x = Math.min(v.x, lower.x);
		lower.y = Math.min(v.y, lower.y);
		lower.z = Math.min(v.z, lower.z);
		upper.x = Math.max(v.x, upper.x);
		upper.y = Math.max(v.y, upper.y);
		upper.z = Math.max(v.z, upper.z);		
	}

	public void setMinMax(Vector3f min, Vector3f max) {
		this.lower = min;
		this.upper = max;
	}

	public Vector3f getExtent() {
		return new Vector3f(getSizeX(), getSizeY(), getSizeZ());
	}

	public Vector3f getCenter() {
		return upper.add(lower).scale(0.5f);
	}
	
	public final void enlargeUlps() {
        final float eps = 0.0001f;
        lower.x -= Math.max(eps, Math.ulp(lower.x));
        lower.y -= Math.max(eps, Math.ulp(lower.y));
        lower.z -= Math.max(eps, Math.ulp(lower.z));
        upper.x += Math.max(eps, Math.ulp(upper.x));
        upper.y += Math.max(eps, Math.ulp(upper.y));
        upper.z += Math.max(eps, Math.ulp(upper.z));
    }
	
	public static BoundingBox merge(BoundingBox boundingBox, BoundingBox b) {
		if(boundingBox == null && b == null)
			return null;
		if(boundingBox == null)
			return b;
		if(b == null)
			return boundingBox;
		float minx = Math.min(b.lower.x, boundingBox.lower.x),
			  miny = Math.min(b.lower.y, boundingBox.lower.y),
			  minz = Math.min(b.lower.z, boundingBox.lower.z),
			  maxx = Math.max(b.upper.x, boundingBox.upper.x),
			  maxy = Math.max(b.upper.y, boundingBox.upper.y),
			  maxz = Math.max(b.upper.z, boundingBox.upper.z);
		return new BoundingBox(new Vector3f(minx,miny,minz), new Vector3f(maxx,maxy,maxz));
	}
}
